/*
 * PhotoSizeAndQualityPanel.java 25 nov 2012
 *
 * Sweet Home 3D, Copyright (c) 2012 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.swing;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.ComponentOrientation;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.Locale;

import javax.swing.DefaultListCellRenderer;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel;
import javax.swing.ToolTipManager;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.eteks.sweethome3d.j3d.Component3DManager;
import com.eteks.sweethome3d.model.AspectRatio;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.tools.OperatingSystem;
import com.eteks.sweethome3d.tools.ResourceURLContent;
import com.eteks.sweethome3d.viewcontroller.AbstractPhotoController;

/**
 * A panel to edit photo size and quality. 
 * @author Emmanuel Puybaret
 */
public class PhotoSizeAndQualityPanel extends JPanel
{
	private JLabel widthLabel;
	private JSpinner widthSpinner;
	private JLabel heightLabel;
	private JSpinner heightSpinner;
	private JCheckBox applyProportionsCheckBox;
	private JComboBox aspectRatioComboBox;
	private JLabel qualityLabel;
	private JSlider qualitySlider;
	private JLabel fastQualityLabel;
	private JLabel bestQualityLabel;
	
	public PhotoSizeAndQualityPanel(Home home, UserPreferences preferences, AbstractPhotoController controller)
	{
		super(new GridBagLayout());
		createComponents(home, preferences, controller);
		setMnemonics(preferences);
		layoutComponents();
		
		preferences.addPropertyChangeListener(UserPreferences.Property.LANGUAGE, new LanguageChangeListener(this));
	}
	
	/**
	 * Creates and initializes components.
	 */
	private void createComponents(final Home home, final UserPreferences preferences,
			final AbstractPhotoController controller)
	{
		// Create width label and spinner bound to WIDTH controller property
		this.widthLabel = new JLabel();
		final SpinnerNumberModel widthSpinnerModel = new SpinnerNumberModel(480, 10, 10000, 10);
		this.widthSpinner = new AutoCommitSpinner(widthSpinnerModel);
		widthSpinnerModel.setValue(controller.getWidth());
		widthSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				controller.setWidth(((Number) widthSpinnerModel.getValue()).intValue());
			}
		});
		controller.addPropertyChangeListener(AbstractPhotoController.Property.WIDTH, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				widthSpinnerModel.setValue(controller.getWidth());
			}
		});
		
		// Create height label and spinner bound to HEIGHT controller property
		this.heightLabel = new JLabel();
		final SpinnerNumberModel heightSpinnerModel = new SpinnerNumberModel(480, 10, 10000, 10);
		this.heightSpinner = new AutoCommitSpinner(heightSpinnerModel);
		heightSpinnerModel.setValue(controller.getHeight());
		heightSpinnerModel.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				controller.setHeight(((Number) heightSpinnerModel.getValue()).intValue());
			}
		});
		controller.addPropertyChangeListener(AbstractPhotoController.Property.HEIGHT, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				heightSpinnerModel.setValue(controller.getHeight());
			}
		});
		
		// Create apply proportions check box bound to ASPECT_RATIO controller property
		boolean notFreeAspectRatio = controller.getAspectRatio() != AspectRatio.FREE_RATIO;
		this.applyProportionsCheckBox = new JCheckBox();
		this.applyProportionsCheckBox.setSelected(notFreeAspectRatio);
		this.applyProportionsCheckBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setAspectRatio(applyProportionsCheckBox.isSelected()
						? (AspectRatio) aspectRatioComboBox.getSelectedItem() : AspectRatio.FREE_RATIO);
			}
		});
		this.aspectRatioComboBox = new JComboBox(new Object[] { AspectRatio.VIEW_3D_RATIO, AspectRatio.SQUARE_RATIO,
				AspectRatio.RATIO_4_3, AspectRatio.RATIO_3_2, AspectRatio.RATIO_16_9, AspectRatio.RATIO_2_1 });
		this.aspectRatioComboBox.setRenderer(new DefaultListCellRenderer()
		{
			@Override
			public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected,
					boolean cellHasFocus)
			{
				AspectRatio aspectRatio = (AspectRatio) value;
				String displayedValue = "";
				if (aspectRatio != AspectRatio.FREE_RATIO)
				{
					switch (aspectRatio)
					{
						case VIEW_3D_RATIO:
							displayedValue = preferences.getLocalizedString(PhotoSizeAndQualityPanel.class,
									"aspectRatioComboBox.view3DRatio.text");
							break;
						case SQUARE_RATIO:
							displayedValue = preferences.getLocalizedString(PhotoSizeAndQualityPanel.class,
									"aspectRatioComboBox.squareRatio.text");
							break;
						case RATIO_4_3:
							displayedValue = "4/3";
							break;
						case RATIO_3_2:
							displayedValue = "3/2";
							break;
						case RATIO_16_9:
							displayedValue = "16/9";
							break;
						case RATIO_2_1:
							displayedValue = "2/1";
							break;
					}
				}
				return super.getListCellRendererComponent(list, displayedValue, index, isSelected, cellHasFocus);
			}
		});
		this.aspectRatioComboBox.addItemListener(new ItemListener()
		{
			public void itemStateChanged(ItemEvent ev)
			{
				controller.setAspectRatio((AspectRatio) aspectRatioComboBox.getSelectedItem());
			}
		});
		this.aspectRatioComboBox.setEnabled(notFreeAspectRatio);
		this.aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
		controller.addPropertyChangeListener(AbstractPhotoController.Property.ASPECT_RATIO, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				boolean notFreeAspectRatio = controller.getAspectRatio() != AspectRatio.FREE_RATIO;
				applyProportionsCheckBox.setSelected(notFreeAspectRatio);
				aspectRatioComboBox.setEnabled(notFreeAspectRatio);
				aspectRatioComboBox.setSelectedItem(controller.getAspectRatio());
			}
		});
		
		// Quality label and slider bound to QUALITY controller property
		this.qualityLabel = new JLabel();
		this.fastQualityLabel = new JLabel();
		this.bestQualityLabel = new JLabel();
		this.qualitySlider = new JSlider(1, controller.getQualityLevelCount())
		{
			@Override
			public String getToolTipText(MouseEvent ev)
			{
				float valueUnderMouse = getSliderValueAt(this, ev.getX(), preferences);
				float valueToTick = valueUnderMouse - (float) Math.floor(valueUnderMouse);
				if (valueToTick < 0.25f || valueToTick > 0.75f)
				{
					// Display a tooltip that explains the different quality levels
					return "<html><table><tr valign='middle'>" + "<td><img border='1' src='"
							+ new ResourceURLContent(PhotoSizeAndQualityPanel.class,
									"resources/quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum())
											+ ".jpg").getURL()
							+ "'></td>" + "<td>"
							+ preferences.getLocalizedString(PhotoSizeAndQualityPanel.class,
									"quality" + Math.round(valueUnderMouse - qualitySlider.getMinimum())
											+ "DescriptionLabel.text")
							+ "</td>" + "</tr></table>";
				}
				else
				{
					return null;
				}
			}
		};
		// Add a listener that displays also the tool tip when user clicks on the slider
		this.qualitySlider.addMouseListener(new MouseAdapter()
		{
			@Override
			public void mousePressed(final MouseEvent ev)
			{
				EventQueue.invokeLater(new Runnable()
				{
					public void run()
					{
						float valueUnderMouse = getSliderValueAt(qualitySlider, ev.getX(), preferences);
						if (qualitySlider.getValue() == Math.round(valueUnderMouse))
						{
							ToolTipManager toolTipManager = ToolTipManager.sharedInstance();
							int initialDelay = toolTipManager.getInitialDelay();
							toolTipManager.setInitialDelay(Math.min(initialDelay, 150));
							toolTipManager.mouseMoved(ev);
							toolTipManager.setInitialDelay(initialDelay);
						}
					}
				});
			}
		});
		this.qualitySlider.setPaintTicks(true);
		this.qualitySlider.setMajorTickSpacing(1);
		this.qualitySlider.setSnapToTicks(true);
		final boolean offScreenImageSupported = Component3DManager.getInstance().isOffScreenImageSupported();
		this.qualitySlider.addChangeListener(new ChangeListener()
		{
			public void stateChanged(ChangeEvent ev)
			{
				if (!offScreenImageSupported)
				{
					// Can't support 2 first quality levels if offscreen image isn't supported 
					qualitySlider.setValue(Math.max(qualitySlider.getMinimum() + 2, qualitySlider.getValue()));
				}
				controller.setQuality(qualitySlider.getValue() - qualitySlider.getMinimum());
			}
		});
		controller.addPropertyChangeListener(AbstractPhotoController.Property.QUALITY, new PropertyChangeListener()
		{
			public void propertyChange(PropertyChangeEvent ev)
			{
				qualitySlider.setValue(qualitySlider.getMinimum() + controller.getQuality());
			}
		});
		this.qualitySlider.setValue(this.qualitySlider.getMinimum() + controller.getQuality());
		
		// Listener on 3D view notified when its size changes
		final JComponent view3D = (JComponent) controller.get3DView();
		final ComponentAdapter view3DSizeListener = new ComponentAdapter()
		{
			@Override
			public void componentResized(ComponentEvent ev)
			{
				controller.set3DViewAspectRatio((float) view3D.getWidth() / view3D.getHeight());
			}
		};
		addAncestorListener(new AncestorListener()
		{
			public void ancestorAdded(AncestorEvent ev)
			{
				view3D.addComponentListener(view3DSizeListener);
				ToolTipManager.sharedInstance().registerComponent(qualitySlider);
			}
			
			public void ancestorRemoved(AncestorEvent ev)
			{
				ToolTipManager.sharedInstance().unregisterComponent(qualitySlider);
				view3D.removeComponentListener(view3DSizeListener);
			}
			
			public void ancestorMoved(AncestorEvent ev)
			{}
		});
		
		setComponentTexts(preferences);
	}
	
	/**
	 * Returns the slider value matching a given x.
	 */
	private float getSliderValueAt(JSlider qualitySlider, int x, UserPreferences preferences)
	{
		int fastLabelOffset = 0;
		int bestLabelOffset = 0;
		int sliderWidth = qualitySlider.getWidth() - fastLabelOffset - bestLabelOffset;
		return qualitySlider.getMinimum() + (float) (x
				- (qualitySlider.getComponentOrientation().isLeftToRight() ? fastLabelOffset : bestLabelOffset))
				/ sliderWidth * (qualitySlider.getMaximum() - qualitySlider.getMinimum());
	}
	
	/**
	 * Sets the texts of the components.
	 */
	private void setComponentTexts(UserPreferences preferences)
	{
		this.widthLabel.setText(
				SwingTools.getLocalizedLabelText(preferences, PhotoSizeAndQualityPanel.class, "widthLabel.text"));
		this.heightLabel.setText(
				SwingTools.getLocalizedLabelText(preferences, PhotoSizeAndQualityPanel.class, "heightLabel.text"));
		this.applyProportionsCheckBox.setText(SwingTools.getLocalizedLabelText(preferences,
				PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.text"));
		this.qualityLabel.setText(
				SwingTools.getLocalizedLabelText(preferences, PhotoSizeAndQualityPanel.class, "qualityLabel.text"));
		this.fastQualityLabel.setText(
				SwingTools.getLocalizedLabelText(preferences, PhotoSizeAndQualityPanel.class, "fastLabel.text"));
		if (!Component3DManager.getInstance().isOffScreenImageSupported())
		{
			this.fastQualityLabel.setEnabled(false);
		}
		this.bestQualityLabel.setText(
				SwingTools.getLocalizedLabelText(preferences, PhotoSizeAndQualityPanel.class, "bestLabel.text"));
	}
	
	/**
	 * Sets components mnemonics and label / component associations.
	 */
	private void setMnemonics(UserPreferences preferences)
	{
		if (!OperatingSystem.isMacOSX())
		{
			this.widthLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(preferences.getLocalizedString(PhotoSizeAndQualityPanel.class, "widthLabel.mnemonic"))
					.getKeyCode());
			this.widthLabel.setLabelFor(this.widthSpinner);
			this.heightLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(
							preferences.getLocalizedString(PhotoSizeAndQualityPanel.class, "heightLabel.mnemonic"))
					.getKeyCode());
			this.heightLabel.setLabelFor(this.heightSpinner);
			this.applyProportionsCheckBox.setMnemonic(KeyStroke.getKeyStroke(
					preferences.getLocalizedString(PhotoSizeAndQualityPanel.class, "applyProportionsCheckBox.mnemonic"))
					.getKeyCode());
			this.qualityLabel.setDisplayedMnemonic(KeyStroke
					.getKeyStroke(
							preferences.getLocalizedString(PhotoSizeAndQualityPanel.class, "qualityLabel.mnemonic"))
					.getKeyCode());
			this.qualityLabel.setLabelFor(this.qualitySlider);
		}
	}
	
	/**
	 * Preferences property listener bound to this panel with a weak reference to avoid
	 * strong link between user preferences and this panel.  
	 */
	public static class LanguageChangeListener implements PropertyChangeListener
	{
		private final WeakReference<PhotoSizeAndQualityPanel> photoPanel;
		
		public LanguageChangeListener(PhotoSizeAndQualityPanel photoPanel)
		{
			this.photoPanel = new WeakReference<PhotoSizeAndQualityPanel>(photoPanel);
		}
		
		public void propertyChange(PropertyChangeEvent ev)
		{
			// If photo panel was garbage collected, remove this listener from preferences
			PhotoSizeAndQualityPanel photoPanel = this.photoPanel.get();
			UserPreferences preferences = (UserPreferences) ev.getSource();
			if (photoPanel == null)
			{
				preferences.removePropertyChangeListener(UserPreferences.Property.LANGUAGE, this);
			}
			else
			{
				photoPanel.setComponentOrientation(ComponentOrientation.getOrientation(Locale.getDefault()));
				photoPanel.setComponentTexts(preferences);
				photoPanel.setMnemonics(preferences);
			}
		}
	}
	
	/**
	 * Layouts panel components in panel with their labels. 
	 */
	private void layoutComponents()
	{
		int labelAlignment = OperatingSystem.isMacOSX() ? JLabel.TRAILING : JLabel.LEADING;
		// First row
		Insets labelInsets = new Insets(0, 0, 0, 5);
		add(this.widthLabel, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, labelInsets, 0, 0));
		// Use HORIZONTAL fill constraint with label alignment to ensure
		// label is correctly sized in small dialogs
		this.widthLabel.setHorizontalAlignment(labelAlignment);
		Insets componentInsets = new Insets(0, 0, 0, 10);
		add(this.widthSpinner, new GridBagConstraints(1, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, componentInsets, 0, 0));
		add(this.heightLabel, new GridBagConstraints(2, 0, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, labelInsets, 0, 0));
		this.heightLabel.setHorizontalAlignment(labelAlignment);
		add(this.heightSpinner, new GridBagConstraints(3, 0, 1, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
		// Second row
		JPanel proportionsPanel = new JPanel();
		proportionsPanel.add(this.applyProportionsCheckBox);
		proportionsPanel.add(this.aspectRatioComboBox);
		add(proportionsPanel, new GridBagConstraints(0, 1, 4, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
		// Third row
		add(this.qualityLabel, new GridBagConstraints(0, 3, 1, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 5), 0, 0));
		this.qualityLabel.setHorizontalAlignment(labelAlignment);
		add(this.qualitySlider, new GridBagConstraints(1, 3, 3, 1, 0, 0, GridBagConstraints.LINE_START,
				GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
		// Fourth row
		JPanel qualityLabelsPanel = new JPanel(new BorderLayout(20, 0));
		qualityLabelsPanel.add(this.fastQualityLabel, BorderLayout.WEST);
		qualityLabelsPanel.add(this.bestQualityLabel, BorderLayout.EAST);
		add(qualityLabelsPanel, new GridBagConstraints(1, 4, 3, 1, 0, 0, GridBagConstraints.CENTER,
				GridBagConstraints.HORIZONTAL, new Insets(OperatingSystem.isWindows() ? 0 : -3, 0, 0, 0), 0, 0));
	}
	
	/**
	 * Enables or disables this panel and its components.
	 */
	@Override
	public void setEnabled(boolean enabled)
	{
		super.setEnabled(enabled);
		this.widthSpinner.setEnabled(enabled);
		this.heightSpinner.setEnabled(enabled);
		this.applyProportionsCheckBox.setEnabled(enabled);
		this.aspectRatioComboBox.setEnabled(enabled);
		this.qualitySlider.setEnabled(enabled);
	}
	
	/**
	 * Enables or disables components that allow to force proportions.
	 */
	public void setProportionsChoiceEnabled(boolean enabled)
	{
		this.applyProportionsCheckBox.setEnabled(enabled);
		this.aspectRatioComboBox.setEnabled(enabled && this.applyProportionsCheckBox.isSelected());
	}
}
